/** 
    Copyright 2010 Henrik Andersson
    
    This file is part of Loopist.

    Loopist is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    Loopist is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with Loopist.  If not, see <http://www.gnu.org/licenses/>.
*/

package se.dinamic.loopist;

import java.lang.Thread;
import java.nio.ShortBuffer;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import android.os.Process;
import android.util.Log;
import android.graphics.Bitmap;
import android.media.AudioManager;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder.AudioSource;
import android.media.AudioTrack;
import android.media.AudioTrack.OnPlaybackPositionUpdateListener;

public class LoopistEngine  implements OnPlaybackPositionUpdateListener, Mixer.Listener {
	/** The count of bars */
	private int _bars;
	/** The count of beats per bar */
	private int _beatsPerBar;
	/** The count of beats per minute*/
	private int _beatsPerMinute;
	/** The count of frames per beat*/	
	private int _framesPerBeat;
	/** The count of millis per beat */
	private int _beatLengthMillis;
	
	private Thread _playbackThread;
	private Thread _recorderThread;
	private Playback _playbackRunnable;
	private Recorder _recorderRunnable;
	private Mixer _mixer;
	private Listener _listener;
	
	/** Create a new engine, bpm,bars and bpb are used
		for templating the looper, a metronome track is created by default..
		\todo Might metronomtrack creation be optional ?
	*/
	public LoopistEngine(int bpm,int bars,int bpb,ByteBuffer clickPcm) {
		
		_bars=bars;
		_beatsPerMinute=bpm;
		_beatsPerBar=bpb;
		_framesPerBeat = (int) (44100.0f / (bpm/60.0f));
		_beatLengthMillis = (int)( (bpm/60.0f)*1000.0f );
		// Setup loopist mixer and loopist tracks
		Track metronome = MetronomeTrack.generateTrack( _beatsPerMinute, _bars, _beatsPerBar, clickPcm ); 
		_mixer=new Mixer( );
		_mixer.setListener( this );
		_mixer.addTrack( new Track(metronome) );
		_mixer.addTrack( new Track(metronome) );
		_mixer.addTrack( new Track(metronome) );
		_mixer.addTrack( metronome );
		
		android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
	
		
		// Setup playback thread
		_playbackRunnable = new Playback( _mixer, this );
		_playbackThread = new Thread( _playbackRunnable );
		_playbackThread.setPriority(Thread.MAX_PRIORITY);
		_playbackThread.start();
		
		// Setup recorder thread
		_recorderRunnable = new Recorder( _playbackRunnable.getBufferSize() );
		_recorderThread = new Thread( _recorderRunnable );
		_recorderThread.setPriority(Thread.MAX_PRIORITY);
		_recorderThread.start();
	}
	
	/** Set listener for LoopistEngine.  \see LoopistEngine.Listener*/
	public void setListener(Listener listener) { _listener=listener; }
	
	/** Start playback */
	public void play() { _playbackRunnable.play(); }
	
	/** Pause playback */
	public void pause() { _playbackRunnable.pause(); }
	
	/** Resumes playback */
	public void resume() { _playbackRunnable.play(); }
	
	/** Prepare for record on specified track. 
		Record is quantized with track and starts when position is equalt to track start.
	*/
	public void recordTrack(int k) {
		if( !_recorderRunnable.isRecording() )
			_recorderRunnable.prepareForRecord( _mixer.getTrack(k) );
	};
	
	public void setTrackListener( int k, Track.Listener listener ) { _mixer.getTrack(k).setListener(listener); }
	public void setTrackVolume(int k, float volume) { _mixer.getTrack(k).setVolume(volume); }
	
	/** Wipe specified track. */
	public void wipeTrack(int k) { _mixer.getTrack(k).wipe(); }
	
	/** Merge a track with another. */
	public void mergeTracks(int k,int l) { 
		_mixer.getTrack(k).merge( _mixer.getTrack(l) ); 
		_mixer.getTrack(l).wipe();
	}
	/** Check if recording is pending */
	public boolean isPendingRecord() { return _recorderRunnable.isPending(); }
	/** Check if recording is performed */
	public boolean isRecording() { return _recorderRunnable.isRecording(); }
	/** Get the track number of recording track */
	public int getRecordingTrack() { return _mixer.getTrackIndex( _recorderRunnable.getActiveTrack() ); }
	
	/** Get the beat length in millis. */
	public int getBeatLength() { return _beatLengthMillis; }
	/** Get a int[3] color array of specified track. */
	public int[] getTrackColor(int k) { return _mixer.getTrack(k).getColor(); }
	/** Get track volume. */
	public float getTrackVolume(int k) { return _mixer.getTrack(k).getVolume(); }
	/**  Get the buffer of a track... */
	public ShortBuffer getTrackBuffer(int k) { return _mixer.getTrack(k).getBuffer(); }
	
	/** Get beat count of a bar. */
	public int getBeatsPerBar() { return _beatsPerBar; }
	
	/** Get beat s per minute. */
	public int getBeatsPerMinute() { return _beatsPerMinute; }
	
	/** Get the length of track in frames... */
	public int getTrackFrameCount() { return _mixer.getTrack(0).getFrameCount(); };
	/** Get bar count. */
	public int getBars() { return _bars; }
	
	/** Get track count */
	public int getTrackCount() { return _mixer.getTrackCount(); }
	
	/** A listener interface of LoopistEngine. */
	static public interface Listener {
		/** Callback for playbackposition 0-1 of full track length */
		public void onPlaybackPosition(float position);
		/** Fired on each bar... */
		public void onBar();
		/** Fired on each beat... */
		public void onBeat();
		
	}
	
	/*
	 * Mixer.Listener Interface
	 */
	public void onBeat() { if( _listener != null ) _listener.onBeat(); }
	public void onBar() { if( _listener != null ) _listener.onBar(); }
	
	public void onTrackEnd() {
		// End of track reached let's check if recorder is pending
		if( _recorderRunnable.isPending() )
			_recorderRunnable.record();
		else if( _recorderRunnable.isRecording() )
			_recorderRunnable.stop();
	}
	
	/** Recorder is a thread job responsible for recording from AudioRecord to an specified track.
	*/
	static private class Recorder implements Runnable {
		private static final int STATE_STOPPED=0;
		private static final int STATE_PENDING=1;
		private static final int STATE_RECORDING=2;
		/** The active track for recording. \see prepareForRecord() */
		private Track _activeTrack;
		/** The recorder state. \see STATE_STOPPED, STATE_PENDING, STATE_RECORDING*/
		private int _recorderState=STATE_STOPPED;
		private int _streamBufferSize=0;
		private AudioRecord _recorder;
		
		/** Buffer for input samples initialized from constructor param bufferSize. */
		private ShortBuffer _inputData;
		
		/** 
			\param bufferSize The size of recording buffer, same as obtained by Playback constructor.
			\todo AudioFormat.CHANNEL_CONFIGURATION_MONO is deprecated 
		*/
		public Recorder( int bufferSize ) {
			// Initialize the recorder...
			
			_streamBufferSize=bufferSize;
			Log.v("Loopist","LoopistEngine() record buffer size "+_streamBufferSize);
			_inputData = ShortBuffer.allocate(_streamBufferSize/2);
			_recorder = new AudioRecord(AudioSource.MIC, 44100, AudioFormat.CHANNEL_CONFIGURATION_MONO, AudioFormat.ENCODING_PCM_16BIT, _streamBufferSize );
			
		}
		
		/** Check if recorder is pending for record */
		public boolean isPending() { return (_recorderState==STATE_PENDING) ? true: false; }
		
		/** Check if recorder is recording */
		public boolean isRecording() { return (_recorderState==STATE_RECORDING) ? true: false; }
		
		/** Get the active recording track number */
		public Track getActiveTrack() { return _activeTrack; };
		
		/** Prepare the recorder for recording to track. */
		public void prepareForRecord( Track track ) {
			_activeTrack = track;
			pending();
		}
		
		/** Sets the recorder in pending state for record. */
		private void pending() { 
			Log.v("Loopist","Recorder.pending()");
			setState( STATE_PENDING );
		}
		
		/** Starts the recording. */
		public void record() { 
			Log.v("Loopist","Recorder.record()");
			_activeTrack.setRecording( true );
			setState( STATE_RECORDING );
			_recorder.startRecording();
		}
		
		/** Stops the recording. */
		public void stop() {
			Log.v("Loopist","Recorder.stop()");
			setState( STATE_STOPPED );
			_activeTrack.setRecording( false );
			_recorder.stop();			
			_activeTrack=null;
		}
		
		/** Changes the state of the recorde an notify thread of change. */
		private void setState(int state) {
			synchronized ( this ) {
				_recorderState = state;
				notify();
			}
		}
		
		@Override
		public void run() {  
				
			while( true ) {
				switch( _recorderState ) {
					
					case STATE_RECORDING:
					{
						_inputData.position(0);
						_recorder.read( _inputData.array(), 0 , _inputData.capacity() );
						_activeTrack.putSamples( _inputData );
					} break;
					
					case STATE_STOPPED:
					case STATE_PENDING:
					default:
					{
						Log.v("Loopist","RecorderRunnable.run() Unknown state, pending or stopped, let's wait for state change notify.");
						// Unhandled state, lets wait for notify from state change
						try {
							synchronized(this) {
								wait();
							}
						} catch(java.lang.InterruptedException e) {
						
						}
					} break;
				}
			}
		}
		
	}
	
	/** Playback is a thread job that is responsible to playback the output of the mixer.
		playback job pulls samples from mixer and fills the output track buffer with frames.
		\see Mixer
	*/
	static private class Playback implements Runnable {
		private  static final int STATE_PLAYBACK=1;
		private static final int STATE_PAUSED=2;
		private static final int PLAYBACK_NOTIFICATION_PERIODS=441;	// Periods notify 10th of second
		private int _playbackState=STATE_PAUSED;
		private int _streamBufferSize;
		private ShortBuffer _outputData;
		private AudioTrack _outputTrack;
		private Mixer _mixer;
		
		/** 
			\param mixer A reference to mixer object from LoopistEngine 
			\param listener A reference to listener for notifications of AudioTrack object.
		*/
		public Playback( Mixer mixer, OnPlaybackPositionUpdateListener listener ) {
			_mixer = mixer;
			
			// Setup playback track and stream buffer size
			_streamBufferSize = AudioTrack.getMinBufferSize(44100, AudioFormat.CHANNEL_CONFIGURATION_MONO , AudioFormat.ENCODING_PCM_16BIT);
			_streamBufferSize = _streamBufferSize;
			_outputData = ShortBuffer.allocate( _streamBufferSize );
			Log.v("Loopist","LoopistEngine() playback buffer size "+_streamBufferSize);
			_outputTrack = new AudioTrack( AudioManager.STREAM_MUSIC, 44100, AudioFormat.CHANNEL_CONFIGURATION_MONO , AudioFormat.ENCODING_PCM_16BIT,_streamBufferSize,AudioTrack.MODE_STREAM);
			_outputTrack.setStereoVolume(AudioTrack.getMaxVolume(),AudioTrack.getMaxVolume());
			_outputTrack.setPlaybackPositionUpdateListener(listener);
			_outputTrack.setPositionNotificationPeriod(PLAYBACK_NOTIFICATION_PERIODS);
		}
		
		public int getBufferSize() { return _streamBufferSize; }
		
		private void setState(int state) {
			synchronized ( this ) {
				_playbackState = state;
				notify();
			}
		}
		
		/** Starts the playback. */
		public void play() { 
			// Prefill playback stream with one buffersize..
			_outputTrack.play(); 
			setState( STATE_PLAYBACK );
		}
		
		/** Pauses the playback.  */
		public void pause() { _outputTrack.pause(); setState( STATE_PAUSED ); }
		
		@Override
		public void run() {  
				
			while( true ) {
				switch( _playbackState ) 
				{
					case STATE_PLAYBACK:
					{
						_mixer.fill( _outputData );
						_outputTrack.write(_outputData.array(),0,_streamBufferSize);
						//_outputTrack.flush();
					} break;
					
					case STATE_PAUSED:
					default:
					{
						Log.v("Loopist","PlaybackRunnable.run() Unknown state or paused, let's wait for state change notify.");
						// Unhandled state, lets wait for notify from state change
						try {
							synchronized(this) {
								wait();
							}
						} catch(java.lang.InterruptedException e) {
							
						}
					} break;
				}
			}
		}
	}
	
	private int _currentFramePointer=0;
	private int _currentBeatCounter=0;
	private int _currentBeatFrameCount=0;
	
	public void onPeriodicNotification(AudioTrack track) {
		_currentFramePointer = (_currentFramePointer+Playback.PLAYBACK_NOTIFICATION_PERIODS) % _mixer.getTrack(0).getFrameCount();
		_currentBeatFrameCount += Playback.PLAYBACK_NOTIFICATION_PERIODS;
		// Let's detect on beat and on bar and pass it to listener...
		if( _currentBeatFrameCount > _framesPerBeat ) {
			_currentBeatFrameCount-=_framesPerBeat;
			if( _listener != null )  _listener.onBeat();
			
			_currentBeatCounter++;
			if( _currentBeatCounter == _beatsPerBar ) {
				_currentBeatCounter=0;
				if( _listener != null )  _listener.onBar();
			}
		}
		
		// Pass on playback position
		if( _listener != null ) 
			_listener.onPlaybackPosition( (float)_currentFramePointer / (float)_mixer.getTrack(0).getFrameCount() );
		
	}
	
	public void onMarkerReached(AudioTrack track){}
}